//  Copyright 2013 Zhengbo Zhou.  All Rights Reserved.
package org.jumbo.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.coremedia.iso.IsoFile;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.imaging.ImageMetadataReader;

/*
 * A class to organize media files.
 * 
 * @author jumboaime@gmail.com (Zhengbo Zhou)
 */
public class MediaOrganizer {
  public static final int THOUSAND = 1000;
  public static final String COPYRIGHT = "Copyright 2013 Jumbo Chow.  All Rights Reserved.\n\n";
  public static final String INTRODUCTION = "Media Organizer is a tool to help you copy image " +
      "or video files into a chronologically organized folder structure.\nThere are two types " +
      "of the directory structure it can build:\n - Type goes first, example:\n" +
      "    Images/yyyy/mm/{dd}/image_file.jpg Videos/yyyy/mm/{dd}/video_file.mp4\n" +
      "- Type goes last, example:\n" + "    yyyy/mm/{dd}/Images/image_file.jpg " +
      "yyyy/mm/{dd}/Videos/video_file.mp4\nRight now the tool supports the following file " +
      "types: .jpg, .jpeg, .png, .gif, .tiff, .mp4, .3gp, .mov, the list will be expanded.\n\n";
  public static final String USAGE = "Usage:\n media-file-organizer.jar " +
      "source_base_dir=base/dir/of/media/files/to/copy " +
      "[target_base_dir=dir/where/files/are/copied/to] [add_day_dir=true|false] " +
      "[type_first=true|false]\n\n";
  public static final String DST_DIR_PATH_DEFAULT =
      System.getProperty("java.io.tmpdir") + File.separator + "dst";
  
  private static Logger logger = Logger.getLogger(
      Thread.currentThread().getStackTrace()[0].getClassName());
  
  private String srcBaseDir;
  private String dstBaseDir;
  private boolean addDayDir;
  // This variable is to determine whether it's Image|Video/YYYY/MM/DD or YYYY/MM/DD/Image|Video
  // May need to come up with something better.
  private boolean typeFirst;

  public String getSrcBaseDir() {
    return srcBaseDir;
  }
  public void setSrcBaseDir(String srcBaseDir) {
    this.srcBaseDir = srcBaseDir;
  }
  public String getDstBaseDir() {
    return dstBaseDir;
  }
  public void setDstBaseDir(String dstBaseDir) {
    this.dstBaseDir = dstBaseDir;
  }
  public boolean isAddDayDir() {
    return addDayDir;
  }
  public void setAddDayDir(boolean addDayDir) {
    this.addDayDir = addDayDir;
  }
  public boolean isTypeFirst() {
    return typeFirst;
  }
  public void setTypeFirst(boolean typeFirst) {
    this.typeFirst = typeFirst;
  }

  public MediaOrganizer() {
    setSrcBaseDir("");
    setDstBaseDir(DST_DIR_PATH_DEFAULT);
    addDayDir = false;
    typeFirst = false;
  }

  /*
   * Organizes the media file into chronologically structured directories.
   */
  public void organizeMediaFile() {
    String fileName;
    long fileSize;
    File[] srcFiles = getSourceFiles();

    if (srcFiles.length == 0) {
      System.out.println("No media file found, exit.");
      System.exit(1);
    }

    for (int i = 0; i < srcFiles.length; i++) {
      if (srcFiles[i].isFile()) {
        fileName = srcFiles[i].getName();
        fileSize = srcFiles[i].length();
        String srcFilePath = getSrcBaseDir() + File.separator + fileName;
        if (fileName.toLowerCase().endsWith(".jpg") ||
            fileName.toLowerCase().endsWith(".png") ||
            fileName.toLowerCase().endsWith(".gif") ||
            fileName.toLowerCase().endsWith(".tiff") ||
            fileName.toLowerCase().endsWith(".jpeg")) {
          copyImageFile(fileName, fileSize, srcFilePath);
        } else if (fileName.toLowerCase().endsWith(".mp4") ||
            fileName.toLowerCase().endsWith(".3gp") ||
            fileName.toLowerCase().endsWith(".mov")) {
          copyVideoFile(fileName, fileSize, srcFilePath);
        } else {
          logger.warning("file type not supported, skip processing: " + fileName);
        }
      }
    }
  }
  private File[] getSourceFiles() {
    File srcDir = new File(getSrcBaseDir());
    if (!srcDir.exists()) {
      logger.severe("source dir is not valid, please rerun with a valid source dir");
      System.exit(1);
    }
    File[] srcFiles = srcDir.listFiles();
    return srcFiles;
  }
  private void copyVideoFile(String fileName, long fileSize, String srcFilePath) {
    logger.info("video file " + fileName);
    try {
      FileChannel fc = new FileInputStream(srcFilePath).getChannel();
      try {
        IsoFile isoFile = new IsoFile(fc);
        Date date = null;
        if (isoFile != null) {
          date = isoFile.getMovieBox().getMovieHeaderBox().getCreationTime();
        }
        createDirAndCopy(fileName, srcFilePath, date, "Videos", fileSize);
        isoFile.close();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
      try {
        fc.close();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    } catch (FileNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  private void copyImageFile(String fileName, long fileSize, String srcFilePath) {
    logger.info("image file " + fileName);
    
    File imageFile = new File(srcFilePath);
    try {
      Metadata imageMetadata = ImageMetadataReader.readMetadata(imageFile);
      ExifSubIFDDirectory directory = imageMetadata.getDirectory(ExifSubIFDDirectory.class);
      Date date = null;
      if (directory != null) {
        date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
      }
      createDirAndCopy(fileName, srcFilePath, date, "Images", fileSize);
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private String appendFileSeparator(String baseDir) {
    return baseDir + File.separator;
  }

  private void createDirAndCopy(String fileName,
      String srcFilePath, Date date, String fileType, long fileSize) throws IOException {
    String destDirPath;
    StringBuilder dstPathBuilder = new StringBuilder(appendFileSeparator(getDstBaseDir()));
    
    if (date != null) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      StringBuilder datePathBuilder = new StringBuilder(
          appendFileSeparator(Integer.toString(calendar.get(Calendar.YEAR))));
      datePathBuilder.append(
          appendFileSeparator(Integer.toString(calendar.get(Calendar.MONTH) + 1)));
      if (isAddDayDir()) {
        datePathBuilder.append(appendFileSeparator(
            Integer.toString(calendar.get(Calendar.DAY_OF_MONTH))));
      }
      if (isTypeFirst()) {
        dstPathBuilder.append(appendFileSeparator(fileType)).append(datePathBuilder.toString());
      } else {
        dstPathBuilder.append(datePathBuilder.toString()).append(appendFileSeparator(fileType));
      }
      destDirPath = dstPathBuilder.toString();
    } else {
      if (isTypeFirst()) {
        dstPathBuilder.append(
            appendFileSeparator(fileType)).append(appendFileSeparator("no_date"));
      } else {
        dstPathBuilder.append(
            appendFileSeparator("no_date")).append(appendFileSeparator(fileType));
      }
      destDirPath = dstPathBuilder.toString();
    }
    File destDir = new File(destDirPath);
    if (!destDir.exists()) {
      if (!destDir.mkdirs()) {
        throw new IOException();
      }
    }
    String destPath = destDirPath + fileName;
    File destFile = new File(destPath);
    if (destFile.exists()) {
      // This dedup algorithm is very naive, need some more investigation on how do we
      // justify if the files are the same.
      logger.info("File already exists, we need to check if they are the same.");
      if (fileSize != destFile.length()) {
        logger.info("File sizes are different, add a sufix");
        logger.warning("Found name collision, add suffix.");
        destPath = destPath + "_1";
      } else {
        logger.info("Files are the same, do nothing.");
      }
    }
    Files.copy(Paths.get(srcFilePath), Paths.get(destPath), StandardCopyOption.COPY_ATTRIBUTES);
  }

  /*
   * Parses command line arguments.
   */
  public void parseArguments(String[] args) {
    for (String arg : args) {
      if (args.length < 1) {
        logger.severe("you need to at least specify source_base_dir");
        System.exit(1);
      }
      String[] argParts = arg.split("=");
      if (argParts[0].equals("source_base_dir")) {
        setSrcBaseDir(argParts[1]);
      }
      if (argParts[0].equals("target_base_dir")) {
        setDstBaseDir(argParts[1]);
      }
      if (argParts[0].equals("add_day_dir")) {
        setAddDayDir(Boolean.parseBoolean(argParts[1]));
      }
      if (argParts[0].equals("type_first")) {
        setTypeFirst(Boolean.parseBoolean(argParts[1]));
      }
    }
    if (getSrcBaseDir().equals("")) {
      logger.severe("source dir not specified, exit!");
      System.out.print(USAGE);
      System.exit(1);
    }    
    logger.info("source: " + getSrcBaseDir() + " target: " + getDstBaseDir());
  }

  public static void main(String[] args) throws SecurityException, IOException {
    // TODO(jumbo): Make sure the exe work (P0)
    // TODO(jumbo): Refactor add MediaFile class to store necessary file metadata. (P2)
	// TODO(jumbo): Multiple thread. (seems not necessary) (P1)
    // TODO(jumbo): Error processing and more user friendly improvements. (P1)
    // TODO(jumbo): Add statistics. (P2)
    // TODO(jumbo): Add unit test. (P2)
    configLogger();
    printStartMessage();
    
    MediaOrganizer organizer = new MediaOrganizer();
    organizer.parseArguments(args);
    System.out.println("Start media file copy.");
    long startTimeNano = System.nanoTime();
    organizer.organizeMediaFile();
    long endTimeNano = System.nanoTime();
    long elapsedTimeSec = (endTimeNano - startTimeNano) / (THOUSAND * THOUSAND * THOUSAND);
    System.out.println("It took " + elapsedTimeSec + " seconds to copy all the files.");
  }
  private static void printStartMessage() {
    System.out.print(COPYRIGHT);
    System.out.print(INTRODUCTION);
    System.out.print(USAGE);
  }
  private static void configLogger() throws IOException {
    FileHandler fh = new FileHandler(System.getProperty("java.io.tmpdir") + File.separator + "organizer.log");
    logger.addHandler(fh);
    logger.setLevel(Level.SEVERE);
  }
}